package joc;

import static org.mockito.Mockito.mock;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import joc.internal.MethodCall;
import joc.internal.MethodCallSequence;
import joc.internal.util.GenericTypeAware;

import org.apache.log4j.Logger;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public abstract class CallSequenceProvider<T> extends GenericTypeAware<T> {
	public final static String createMethodCallSequences = "createMethodCallSequences";
	public final static String verifyCall = "verifyCall";

	private Logger logger = Logger.getLogger(getClass());

	private T target;
	private CallSequenceOperation lastOperation;
	private Answer<?> mockAnswer = new CallSequenceAnswer<Object>();
	private List<MethodCallSequence> methodCallSequences = new ArrayList<MethodCallSequence>();
	private MethodCallSequence currentSequence;
	private List<MethodCallSequence> currentPossibleSequences = new ArrayList<MethodCallSequence>();
	private List<MethodCall> allowAnywhere = new ArrayList<MethodCall>();

	private enum CallSequenceOperation {
		CALL, BEFORE, ALLOW_ANYWHERE, ALLOW_ANYWHERE_WITHIN_THIS_SEQUENCE;
	}

	private class CallSequenceAnswer<U> implements Answer<U> {
		@Override
		public U answer(InvocationOnMock invocation) throws Throwable {
			MethodCall methodCall = new MethodCall(invocation.getMethod().getName());
			if (lastOperation == CallSequenceOperation.CALL) {
				currentSequence = new MethodCallSequence(methodCall);
				methodCallSequences.add(currentSequence);
			} else if (lastOperation == CallSequenceOperation.BEFORE) {
				if (currentSequence == null) {
					logger.error("before() is only allowed after a sequence has been started with call().");
					return null;
				}
				currentSequence.addCall(methodCall);
			} else if (lastOperation == CallSequenceOperation.ALLOW_ANYWHERE) {
				allowAnywhere.add(methodCall);
			} else if (lastOperation == CallSequenceOperation.ALLOW_ANYWHERE_WITHIN_THIS_SEQUENCE) {
				if (currentSequence == null) {
					logger.error("allowAnywhereWithinThisSequence() is only allowed after a sequence has been started with call().");
					return null;
				}
				currentSequence.addAllowAnywhere(methodCall);
			}
			return null;
		}
	}

	public CallSequenceProvider() {
		target = mock(getGenericClass(), mockAnswer);
		callSequence(target);
		logger.debug("extracted the following sequences:");
		for (MethodCallSequence sequence : methodCallSequences) {
			logger.debug("new sequence:");
			for (MethodCall call : sequence.getMethodCalls()) {
				logger.debug(call.getMethodName());
			}
			logger.debug("allow anywhere in this sequence:");
			for (MethodCall call : sequence.getAllowAnywhere()) {
				logger.debug(call.getMethodName());
			}
		}
		logger.debug("global allow anywhere:");
		for (MethodCall call : allowAnywhere) {
			logger.debug(call.getMethodName());
		}
	}

	public final synchronized boolean verifyCall(MethodCall methodCall) {
		logger.debug("verifying call to " + methodCall.getMethodName());
		if (allowAnywhere.contains(methodCall)) {
			logger.debug("is allowed anywhere");
			return true;
		}
		if (currentPossibleSequences.isEmpty()) {
			logger.debug("building new possible sequences");
			for (MethodCallSequence sequence : methodCallSequences) {
				if (sequence.beginsWith(methodCall)) {
					logger.debug("adding sequence, as it begins with " + methodCall.getMethodName());
					currentPossibleSequences.add(sequence);
				}
			}
		}
		for (Iterator<MethodCallSequence> iterator = currentPossibleSequences.iterator(); iterator.hasNext();) {
			MethodCallSequence sequence = iterator.next();
			if (!sequence.verifyCall(methodCall)) {
				logger.debug("verify returned false, removing sequence");
				iterator.remove();
			}
		}
		if (currentPossibleSequences.isEmpty()) {
			logger.debug("no possible sequences left, returning false");
			return false;
		}
		for (Iterator<MethodCallSequence> iterator = currentPossibleSequences.iterator(); iterator.hasNext();) {
			MethodCallSequence sequence = iterator.next();
			if (sequence.isOnLastCall()) {
				logger.debug("cleaning up sequences, removing");
				iterator.remove();
			}
		}
		logger.debug("returning true");
		return true;
	}

	public abstract void callSequence(T target);

	public final T call() {
		lastOperation = CallSequenceOperation.CALL;
		return target;
	}

	public final T before() {
		lastOperation = CallSequenceOperation.BEFORE;
		return target;
	}

	public final T allowAnywhere() {
		lastOperation = CallSequenceOperation.ALLOW_ANYWHERE;
		return target;
	}

	public final T allowAnywhereWithinThisSequence() {
		lastOperation = CallSequenceOperation.ALLOW_ANYWHERE_WITHIN_THIS_SEQUENCE;
		return target;
	}

}
